资源
- 官网在线编译页面 | TypeScript Playground。 本书特别适合新人入门
TS
,本章做为工具书记录要点。
1 - 简介
类型(type)是人为添加的一种编程约束和用法提示
静态类型的优点
- 有利于代码的静态分析。不必运行代码,便可发现错误。
- 更好的 IDE 支持,做到语法提示和自动补全。
- 提供了代码文档。
- 有助于代码重构。
// ==== 1
let obj = { message: '' };
console.log(obj.messege); // 报错 messege 拼错。 在 js 中不会报错,在 ts 报错
// ==== 2
function hello() {
return 'hello world';
}
hello().find('hello'); // 报错 没有 find。 在 js 中不会报错,在 ts 报错
// ==== 3
let y = { foo: 1 };
delete y.foo; // 报错 在 js 中不会报错,对象的属性也是静态的,不允许随意增删
y.bar = 2; // 报错 在 js 中不会报错,对象的属性也是静态的,不允许随意增删
静态类型的缺点
- 丧失了动态类型的代码灵活性。
- 增加了编程工作量。
- 更高的学习成本。
- 引入了独立的编译步骤。将 TS 代码转成 JS 代码,才能运行。
2 - 基本用法
类型声明
// 变量: 类型
let foo:string;
// ====
// 参数 返回值也一样
function toString(num:number):string {
return String(num);
}
类型推断
类型声明并不是必需的, TypeScript 会自己推断类型。
let foo = 123;
foo = 'hello'; // 报错 推断为 number,更改类型匹配错误
// ====
// 函数返回值也可以推断
function toString(num:number) {
return String(num);
}
因为 TypeScript 的类型推断,所以函数返回值的类型通常是省略不写的。
TypeScript 的设计思想是,类型声明是可选的,你可以加,也可以不加。即使不加类型声明,依然是有效的 TypeScript 代码。由于这个原因,所有 JavaScript 代码都是合法的 TypeScript 代码,只是这时不能保证 TypeScript 会正确推断出类型。 这样设计还有一个好处,将以前的 JavaScript 项目改为 TypeScript 项目时,你可以逐步地为老代码添加类型,即使有些代码没有添加,也不会无法运行。
编译
编译:TS 转 JS 的过程
编译时,会将类型声明和类型相关的代码全部删除,只留下能运行的 JS 代码,并且不会改变 JS 的运行结果。
所以,TypeScript 的类型检查只是编译时的类型检查,编译后不再检验
值与类型
“类型”是针对“值”的,可以视为是后者的一个元属性,如:3
是一个值,它的类型是 number
。 TypeScript 代码只涉及类型,不涉及值。
TypeScript 项目里面,其实存在两种代码,一种是底层的“值代码”,另一种是上层的“类型代码”。它们是可以分离的,TypeScript 的编译过程,实际上就是把“类型代码”全部拿掉,只保留“值代码”。
TypeScript Playground
官网在线编译页面 | TypeScript Playground。 用于在线编译成 js
代码,学习 ts
时强烈推荐使用
tsc 编译器
官方提供的编译器: tsc
tsc 的作用就是把 .ts
脚本转变成 .js
脚本。
$ npm install -g typescript
$ tsc -v
Version 5.1.6
$ tsc file1.ts file2.ts --outFile ./dist/app.js --target es2015
# 将`file1.ts`和`file2.ts`两个脚本编译成`dist`子目录下的一个 js 文件`app.js`, 转换成 es6 语法(默认es3)
# 也可以不指定 outFile 而是使用 --outDir dist 输出同名文件
编译错误处理
编译报错,tsc
命令会显示报错信息,但依然会编译生成 JavaScript 脚本。
编译器的作用只是给出编译错误,至于怎么处理这些错误,那就是开发者自己的判断了
--noEmitOnError
: 一旦报错就停止编译,不生成编译产物
--noEmit
: 只检查类型是否正确,不生成 JavaScript 文件。
维护成配置文件 tsconfig.json
执行 tsc
即可
{
"files": ["file1.ts", "file2.ts"],
"compilerOptions": {
"outFile": "dist/app.js"
}
}
ts-node
模块
非官方 npm
模块,可以直接运行 TypeScript
代码
npm install -g ts-node
ts-node script.ts
# 或者 npx
# `npx`会在线调用 ts-node,从而在不安装的情况下,运行`script.ts`
npx ts-node script.ts
3 - any、unknown、never 类型
TypeScript
有两个“顶层类型”( any
、unknown
),但是“底层类型”只有 never
唯一一个。
any 基本定义
any
类型表示没有任何限制,可以赋予任意类型的值。
TypeScript 实际上会关闭这个变量的类型检查,由于这个原因,应该尽量避免使用
any
类型,否则就失去了使用 TypeScript 的意义。
any
缺点:
- 关闭了类型校验
- 通过赋值,污染其他具有正确类型的变量,把错误留到运行时。
any
类型主要适用以下两个场合
- 出于特殊原因,需要关闭某些变量的类型检查。
- 适配以前老的 JavaScript 项目,让代码快速迁移到 TypeScript。
any
类型可以看成是所有其他类型的全集,包含了一切可能的类型。TypeScript
将这种类型称为“顶层类型”(top type),涵盖了所有下层。
any 类型推断问题
变量没有指定类型、TypeScript 会自己推断类型,如果无法推断出,TypeScript 就会认为该变量的类型是 any
。
TypeScript 提供 --noImplicitAny
,只要推断出any
类型就会报错。
$ tsc --noImplicitAny app.ts
变量请务必声明类型,避免推断为 any
any 污染问题
any
因为没有类型检查,可以赋值给其他任何类型的变量,导致其他变量出错。
let x:any = 'hello';
let y:number;
y = x; // 不报错
y * 123 // 不报错
y.toFixed() // 不报错
通过赋值,污染其他具有正确类型的变量,把错误留到运行时。
unknown 类型
产生原因:为了解决 any
类型“污染”其他变量的问题。
具有以下特点:
unknown
与any
含义相同:表示类型不确定,可能是任意类型。- 不能直接赋值给其他类型的变量,赋值给
any
和unknown
以外类型的变量都会报错( 目的是避免运行时错误,因为无法确保该变量具备调用的方法或属性。) - 仅能少数运算符(
==
、===
、!=
、!==
、||
、&&
、?
、!
、typeof
、instanceof
)
// 与 `any` 含义相同:表示类型不确定,可能是任意类型。
let x:unknown;
x = true; // 正确
x = 42; // 正确
// 不能直接赋值给其他类型的变量,赋值给 `any` 和 `unknown` 以外类型的变量都会报错
let v:unknown = 123;
let v1:boolean = v; // 报错
let v2:unknown = 'hello';
// 直接调用`unknown`类型变量的属性和方法,或者直接当作函数执行,都会报错。
let v1:unknown = { foo: 123 };
v1.foo // 报错
let v2:unknown = 'hello';
v2.trim() // 报错
let v3:unknown = (n = 0) => n + 1;
v3() // 报错
// 仅能少数运算符(`==`、`===`、`!=`、`!==`、`||`、`&&`、`?`、`!`、`typeof`、`instanceof`)
let a:unknown = 1;
a + 1 // 报错
a === 1 // 正确
限制这么多, unknown
有什么用? 经过 typeof
进行 “类型缩小”, unknown
类型变量才可以使用,确保不会出错。
let a:unknown = 1;
if (typeof a === 'number') {
let r = a + 10; // 正确
}
只有明确 unknown
变量的实际类型,才允许使用它,防止像 any
“污染” 其他变量。
凡是需要设为
any
类型的地方,通常都应该优先考虑设为unknown
类型。
never 类型
“空类型”,该类型为空,不包含任何值。当然不可能存在这样的值,所以称为 never
。
let x:never;
特点:
- 不能赋值给它任何值,否则都会报错。
- 可以赋值给任意其他类型。(空集是任何集合的子集,任何类型都包含了
never
类型,never
为底层类型(bottom type
) )
function f():never { // 报错不可能返回值,所以 never
throw new Error('Error');
}
let v1:number = f(); // 不报错
let v2:string = f(); // 不报错
let v3:boolean = f(); // 不报错
使用场景:
- 类型运算之中,保证类型运算的正确性和可预测性。
- 不可能返回值的函数,返回值的类型就可以写成
never
。例如函数内throw Error
了。 - 联合类型,通常需要使用分支处理每一种类型。这时,处理所有可能的类型之后,剩余的情况就属于
never
类型。
// 联合类型分支处理
function fn(x:string|number) {
if (typeof x === 'string') {
// ...
} else if (typeof x === 'number') {
// ...
} else {
x; // never 类型
}
}
never
在类型运算之中,保证类型运算的正确性和可预测性:
- 当两个类型进行交叉类型运算(使用 & 符号)时,其中一个类型是 never 类型,结果将始终为 never 类型。
- 当两个类型进行联合类型运算(使用 | 符号)时,其中一个类型是 never 类型,结果类型将保留自身的类型。 因此,never 类型在类型运算中起到了保证完整性的作用,防止出现意外的结果。它提醒开发者在类型运算中考虑边界情况,确保结果的正确性和可预测性。
4 - 类型系统
基本类型
ts
继承 js
8 种基本类型:
- 原始类型(primitive value):
- boolean
- string
- number
- bigint
- symbol
- 复合类型:
- object
- 特殊值:
- undefined
- null
基础类型的名称都是小写字母,undefined
和 null
既可以为值,也可以为类型,取决于在哪使用。
复杂类型由这 8 种基础类型组成。
const x:boolean = true;
const y:boolean = false;
const x:string = 'hello';
const y:string = `${x} world`;
const x:number = 123;
const y:number = 3.14;
const z:number = 0xffff;
const x:bigint = 123n;
const y:bigint = 0xffffn;
// `bigint`类型赋值为整数和小数,都会报错。
const x:bigint = 123; // 报错
const y:bigint = 3.14; // 报错
const x:symbol = Symbol();
// 对象、数组、函数都属于 object 类型
const x:object = { foo: 123 };
const y:object = [1, 2, 3];
const z:object = (n:number) => n + 1;
// `undefined` 和 `null` 既可以为值,也可以为类型,取决于在哪使用。
let x:undefined = undefined;
const x:null = null;
// 如果不声明类型,赋值为`undefined`或`null`,默认推断类型为 any
// 关闭 noImplicitAny 和 strictNullChecks
let a = undefined; // any
const b = undefined; // any
let c = null; // any
const d = null; // any
// 想要准确推断,使用 strictNullChecks
// tsc --strictNullChecks app.ts
let a = undefined; // undefined
const b = undefined; // undefined
let c = null; // null
const d = null; // null
包装对象类型
5 种原始类型(primitive value)的值,都有对应的包装对象(wrapper object)。 包装对象:指的是这些值在需要时,会自动产生的对象。
// js 中的原始类型在执行时,会自动转为包装对象
// `charAt()`方法其实是定义在包装对象上
// 这样的设计,去了将原始类型的值手动转成对象实例的麻烦。
'hello'.charAt(1) // 'e'
symbol
类型和 bigint
没有构造函数,即 new Symbol()
和 new BigInt()
剩余的boolean、string、number 都有。
包装对象类型与字面量类型
包装对象的存在导致每一个原始类型的值都有【包装对象】和【字面量】两种情况。
'hello' // 字面量
new String('hello') // 包装对象
为了区分这两种情况,TypeScript
对五种原始类型分别提供了【大写】和【小写】两种类型
- 大写类型:同时包含包装对象和字面量两种情况
- 小写类型:只包含字面量,不包含包装对象
const s1:String = 'hello';
const s2:String = new String('hello');
const s3:string = 'hello';
const s4:string = new String('hello'); // 报错
建议只使用小写类型,使用包装对象的场景特别少 而且 TypeScript
把很多内置方法的参数,定义成小写类型,使用大写类型会报错。
const n1:number = 1;
Math.abs(n1)
const n2:Number = 1;
Math.abs(n2) // 报错
Object 类型与 object 类型
大写的 Object
类型代表 JavaScript 语言里面的广义对象,除了 undefined
和 null
, 囊括了几乎所有的值。
let obj:Object;
// 以下都是合法的
obj = true;
obj = 'hi';
obj = 1;
obj = { foo: 123 };
obj = [1, 2];
obj = (a:number) => a + 1;
// 除了以下
obj = undefined; // 报错
obj = null; // 报错
空对象 {}是
Object` 类型的简写形式,平时常用空对象代替。
let obj:{};
// 等同下面
let obj:Object;
无所不包的
Object
类型既不符合直觉,也不方便使用。
小写的 object
类型代表 JavaScript 里面的狭义对象,即可以用字面量表示的对象,只包含对象、数组和函数,不包括原始类型的值。
let obj:object;
obj = { foo: 123 };
obj = [1, 2];
obj = (a:number) => a + 1;
obj = true; // 报错
obj = 'hi'; // 报错
obj = 1; // 报错
建议总是使用小写类型
object
,不使用大写类型Object
大写的 Object
类型,和小写的 object
类型,都只包含 JavaScript 内置对象原生的属性和方法,用户自定义的属性和方法都不存在于这两个类型之中。
const o1:Object = { foo: 0 };
const o2:object = { foo: 0 };
o1.toString()
o1.foo // 报错
o2.toString()
o2.foo // 报错
// 描述对象的自定义属性,在后续有解决方案, 可以搜索 【对象类型】
undefined 和 null 的特殊性
undefined
表示还没有赋值,null
表示值为空。 任何其他类型的变量都可以赋值为 undefined
或 null
。
let age:number = 24;
age = null; // 正确
age = undefined; // 正确
并不是因为
undefined
和null
包含在number
类型里面,而是故意这样设计,任何类型的变量都可以赋值为undefined
和null
,以便跟 JavaScript 的行为保持一致。
但这种设计带来了弊端
const obj:object = undefined;
obj.toString() // 编译不报错,运行就报错
为了解决这个问题,可以打开 strictNullChecks
。undefined
和 null
就不能赋值给除了 any
类型和 unknown
类型之外的变量。
// tsc --strictNullChecks app.ts
let age:number = 24;
age = null; // 报错
age = undefined; // 报错
let x:undefined = null; // 报错
let y:null = undefined; // 报错
let x:any = undefined;
let y:unknown = null;
或者配置项
// tsconfig.json
{
"compilerOptions": {
"strictNullChecks": true
// ...
}
}
值类型
单个值也是一种类型,称为“值类型”。
let x:'hello';
x = 'hello';
x = 'world'; // 报错
const
声明的变量,如果没有注明类型,会推断该变量为值类型。
// x 的类型是 "https"
const x = 'https';
// y 的类型是 string
const y:string = 'https';
如果赋值为对象,不会推断为值类型。因为 JavaScript 里面,const
变量赋值为对象时,属性值是可以改变的。
// x 的类型是 { foo: number }
const x = { foo: 1 };
父子类型赋值关系
const x:5 = 4 + 1; // 报错
// 等号左侧的类型是数值`5`,等号右侧`4 + 1`的类型,推测为`number`
let x:5 = 5;
let y:number = 4 + 1;
// 5 是 number 子类 父类型变量可以赋值子类型
y = x;
// number 不是 5 子类 子类型变量不可以赋值父类型,毕竟子类型范围比较小
x = y; // 报错
// 如果要让子类型变量可以赋值父类型,需要使用到类型断言
const x:5 = (4 + 1) as 5;
// `as 5` 告诉编译器,可以把 `4 + 1` 的类型视为值类型 `5`
联合类型
联合类型(union types):多个类型组成的新类型,使用符号 |
表示。 A|B
:只要属于 A
或 B
,就属于联合类型 A|B
。
let x:string|number;
x = 123;
x = 'abc';
联合类型可以与值类型相结合,表示一个变量的值有若干种可能
let setting:true|false;
let gender:'male'|'female';
let rainbowColor:'赤'|'橙'|'黄'|'绿'|'青'|'蓝'|'紫';
场景:打开了 strictNullChecks
配置后,如果某个变量确实可能包含空值,就可以采用联合类型的写法。
let name:string|null;
name = 'John';
name = null;
// 多行的写法,减少心智负担
let x:
| 'one'
| 'two'
| 'three'
| 'four';
联合类型是一种“类型放大”(type widening),多类型的联合类型在使用时,需要进行“类型缩小”(type narrowing),否则报错。
function printId(
id:number|string
) {
console.log(id.toUpperCase()); // 报错
}
// 类型缩小才不报错
function printId(
id:number|string
) {
if (typeof id === 'string') {
console.log(id.toUpperCase());
} else {
console.log(id);
}
}
// 另一种类型缩小
function getPort(
scheme: 'http'|'https'
) {
switch (scheme) {
case 'http':
return 80;
case 'https':
return 443;
}
}
交叉类型
交叉类型(intersection types) ,A&B
:即满足 A
又满足 B
let x:number&string; // never 因为不可能同时满足
交叉类型主要用于表示对象的合成
let obj:
{ foo: string } &
{ bar: string };
obj = {
foo: 'hello',
bar: 'world'
};
// 用来为对象类型添加新属性
type A = { foo: number };
type B = A & { bar: number };
// 等同于
type B = { foo: number, bar: number };